/** * Flex block tool for Editor.js. * Allows users to create highly custom Flexbox layouts with alignment, direction, gaps, and custom column items. * Supports instantiating other registered plugins inside columns dynamically. */ class FlexTool { static get toolbox() { return { title: 'Flex Layout', icon: '' }; } constructor({ data, api, readOnly }) { this.api = api; this.readOnly = readOnly; this.data = { cols: parseInt(data.cols) || 2, direction: data.direction || 'row', justify: data.justify || 'start', align: data.align || 'stretch', gap: data.gap || '3', // standard Bootstrap gap level globalClass: data.globalClass || '', globalId: data.globalId || '', globalAttributes: data.globalAttributes || data.attributes || '' }; this.activeInstances = {}; // Load items content dynamically for (let i = 1; i <= 12; i++) { this.data[`type${i}`] = data[`type${i}`] || 'html'; this.data[`col${i}`] = data[`col${i}`] || ''; this.data[`imgUrl${i}`] = data[`imgUrl${i}`] || ''; this.data[`ytUrl${i}`] = data[`ytUrl${i}`] || ''; this.data[`accTitle${i}`] = data[`accTitle${i}`] || ''; this.data[`accContent${i}`] = data[`accContent${i}`] || ''; this.data[`class${i}`] = data[`class${i}`] || ''; this.data[`id${i}`] = data[`id${i}`] || ''; this.data[`attr${i}`] = data[`attr${i}`] || data[`attributes${i}`] || ''; this.data[`width${i}`] = parseInt(data[`width${i}`]) || 0; // 0 means auto this.data[`caption${i}`] = data[`caption${i}`] || ''; } this.wrapper = undefined; } render() { this.wrapper = document.createElement('div'); this.wrapper.className = 'p-3 bg-light border rounded mb-3 ce-flex-tool-wrapper'; this.wrapper.style.fontFamily = 'inherit'; const title = document.createElement('div'); title.className = 'font-weight-bold text-primary small mb-3'; title.innerHTML = ' Custom Flexbox Layout Builder'; this.wrapper.appendChild(title); // Container Flex Settings Row const settingsRow = document.createElement('div'); settingsRow.className = 'form-row mb-3 pb-3 border-bottom'; // 1. Direction const dirCol = document.createElement('div'); dirCol.className = 'col-md-3 col-sm-6 mb-2'; dirCol.innerHTML = ''; const dirSelect = document.createElement('select'); dirSelect.className = 'custom-select custom-select-sm'; const dirOpts = [ { val: 'row', lbl: 'Row' }, { val: 'row-reverse', lbl: 'Row Reverse' }, { val: 'column', lbl: 'Column' }, { val: 'column-reverse', lbl: 'Column Reverse' } ]; dirOpts.forEach(o => { const opt = document.createElement('option'); opt.value = o.val; opt.innerText = o.lbl; opt.selected = this.data.direction === o.val; dirSelect.appendChild(opt); }); dirSelect.addEventListener('change', (e) => this.data.direction = e.target.value); dirCol.appendChild(dirSelect); settingsRow.appendChild(dirCol); // 2. Justify Content const justifyCol = document.createElement('div'); justifyCol.className = 'col-md-3 col-sm-6 mb-2'; justifyCol.innerHTML = ''; const justifySelect = document.createElement('select'); justifySelect.className = 'custom-select custom-select-sm'; const justifyOpts = [ { val: 'start', lbl: 'Start' }, { val: 'center', lbl: 'Center' }, { val: 'end', lbl: 'End' }, { val: 'between', lbl: 'Space Between' }, { val: 'around', lbl: 'Space Around' }, { val: 'evenly', lbl: 'Space Evenly' } ]; justifyOpts.forEach(o => { const opt = document.createElement('option'); opt.value = o.val; opt.innerText = o.lbl; opt.selected = this.data.justify === o.val; justifySelect.appendChild(opt); }); justifySelect.addEventListener('change', (e) => this.data.justify = e.target.value); justifyCol.appendChild(justifySelect); settingsRow.appendChild(justifyCol); // 3. Align Items const alignCol = document.createElement('div'); alignCol.className = 'col-md-3 col-sm-6 mb-2'; alignCol.innerHTML = ''; const alignSelect = document.createElement('select'); alignSelect.className = 'custom-select custom-select-sm'; const alignOpts = [ { val: 'stretch', lbl: 'Stretch' }, { val: 'start', lbl: 'Start' }, { val: 'center', lbl: 'Center' }, { val: 'end', lbl: 'End' } ]; alignOpts.forEach(o => { const opt = document.createElement('option'); opt.value = o.val; opt.innerText = o.lbl; opt.selected = this.data.align === o.val; alignSelect.appendChild(opt); }); alignSelect.addEventListener('change', (e) => this.data.align = e.target.value); alignCol.appendChild(alignSelect); settingsRow.appendChild(alignCol); // 4. Gap Level const gapCol = document.createElement('div'); gapCol.className = 'col-md-3 col-sm-6 mb-2'; gapCol.innerHTML = ''; const gapSelect = document.createElement('select'); gapSelect.className = 'custom-select custom-select-sm'; const gapOpts = [ { val: '0', lbl: 'None' }, { val: '1', lbl: 'Extra Small' }, { val: '2', lbl: 'Small' }, { val: '3', lbl: 'Medium' }, { val: '4', lbl: 'Large' }, { val: '5', lbl: 'Extra Large' } ]; gapOpts.forEach(o => { const opt = document.createElement('option'); opt.value = o.val; opt.innerText = o.lbl; opt.selected = this.data.gap === o.val; gapSelect.appendChild(opt); }); gapSelect.addEventListener('change', (e) => this.data.gap = e.target.value); gapCol.appendChild(gapSelect); settingsRow.appendChild(gapCol); this.wrapper.appendChild(settingsRow); // Background Settings Row const bgSettingsRow = document.createElement('div'); bgSettingsRow.className = 'form-row mb-3 pb-2 border-bottom'; // 1. Flex Items (Cols) const colDiv = document.createElement('div'); colDiv.className = 'col-md-2 mb-2'; colDiv.innerHTML = ''; const input = document.createElement('input'); input.type = 'number'; input.className = 'form-control form-control-sm'; input.min = '1'; input.max = '12'; input.value = this.data.cols; if (this.readOnly) input.disabled = true; input.addEventListener('input', (e) => { let val = parseInt(e.target.value) || 2; if (val < 1) val = 1; if (val > 12) val = 12; this.data.cols = val; this._renderColumnInputs(); }); colDiv.appendChild(input); bgSettingsRow.appendChild(colDiv); // 2. Global CSS Class const classDiv = document.createElement('div'); classDiv.className = 'col-md-3 mb-2'; classDiv.innerHTML = ''; const classInput = document.createElement('input'); classInput.type = 'text'; classInput.className = 'form-control form-control-sm'; classInput.placeholder = 'e.g. my-flex-row'; classInput.value = this.data.globalClass || ''; if (this.readOnly) classInput.disabled = true; classInput.addEventListener('input', (e) => this.data.globalClass = e.target.value.trim()); this.globalClassInput = classInput; classDiv.appendChild(classInput); bgSettingsRow.appendChild(classDiv); // 3. Global HTML ID const idDiv = document.createElement('div'); idDiv.className = 'col-md-3 mb-2'; idDiv.innerHTML = ''; const idInput = document.createElement('input'); idInput.type = 'text'; idInput.className = 'form-control form-control-sm'; idInput.placeholder = 'e.g. flex-section-1'; idInput.value = this.data.globalId || ''; if (this.readOnly) idInput.disabled = true; idInput.addEventListener('input', (e) => this.data.globalId = e.target.value.trim()); this.globalIdInput = idInput; idDiv.appendChild(idInput); bgSettingsRow.appendChild(idDiv); // 4. Global Custom Attributes const attrDiv = document.createElement('div'); attrDiv.className = 'col-md-4 mb-2'; attrDiv.innerHTML = ''; const attrInput = document.createElement('input'); attrInput.type = 'text'; attrInput.className = 'form-control form-control-sm'; attrInput.placeholder = 'e.g. data-aos="fade-up" style="background:#fff"'; attrInput.value = this.data.globalAttributes || ''; if (this.readOnly) attrInput.disabled = true; attrInput.addEventListener('input', (e) => this.data.globalAttributes = e.target.value.trim()); this.globalAttrInput = attrInput; attrDiv.appendChild(attrInput); bgSettingsRow.appendChild(attrDiv); this.wrapper.appendChild(bgSettingsRow); // Columns inputs container this.inputsContainer = document.createElement('div'); this.wrapper.appendChild(this.inputsContainer); this._renderColumnInputs(); return this.wrapper; } _renderColumnInputs() { this.inputsContainer.innerHTML = ''; const row = document.createElement('div'); row.className = 'row'; const count = this.data.cols; const colWidthClass = count === 1 ? 'col-12' : (count === 2 ? 'col-md-6' : (count === 3 ? 'col-md-4' : (count === 4 ? 'col-md-3' : 'col-md'))); for (let i = 1; i <= count; i++) { const colDiv = document.createElement('div'); colDiv.className = `${colWidthClass} mb-3 p-2 border rounded bg-white`; colDiv.style.boxShadow = '0 1px 3px rgba(0,0,0,0.05)'; const headerRow = document.createElement('div'); headerRow.className = 'd-flex justify-content-between align-items-center mb-2 pb-1 border-bottom'; const label = document.createElement('label'); label.className = 'small font-weight-bold text-dark mb-0'; label.innerText = `Item ${i}`; headerRow.appendChild(label); const controlsWrapper = document.createElement('div'); controlsWrapper.className = 'd-flex align-items-center gap-1'; // Width Selector const widthSelect = document.createElement('select'); widthSelect.className = 'custom-select custom-select-sm mr-1'; widthSelect.style.width = '75px'; if (this.readOnly) widthSelect.disabled = true; const widthOpts = [ { val: 0, lbl: 'Auto' }, { val: 1, lbl: '1/12' }, { val: 2, lbl: '2/12' }, { val: 3, lbl: '3/12' }, { val: 4, lbl: '4/12' }, { val: 5, lbl: '5/12' }, { val: 6, lbl: '6/12' }, { val: 7, lbl: '7/12' }, { val: 8, lbl: '8/12' }, { val: 9, lbl: '9/12' }, { val: 10, lbl: '10/12' }, { val: 11, lbl: '11/12' }, { val: 12, lbl: '12/12' } ]; widthOpts.forEach(o => { const opt = document.createElement('option'); opt.value = o.val; opt.innerText = o.lbl; opt.selected = this.data[`width${i}`] === o.val; widthSelect.appendChild(opt); }); widthSelect.addEventListener('change', (e) => { this.data[`width${i}`] = parseInt(e.target.value) || 0; }); controlsWrapper.appendChild(widthSelect); // Column Type Select (Built-ins + Registered plugins dynamically) const typeSelect = document.createElement('select'); typeSelect.className = 'custom-select custom-select-sm'; typeSelect.style.width = '100px'; if (this.readOnly) typeSelect.disabled = true; const types = [ { value: 'html', label: 'HTML/Text' }, { value: 'image', label: 'Image' }, { value: 'youtube', label: 'YouTube' }, { value: 'accordion', label: 'Accordion' } ]; if (window.SISEditorPlugins) { Object.keys(window.SISEditorPlugins).forEach(key => { // Allow nesting any registered plugins (no exclusions) if (!types.some(t => t.value === key)) { types.push({ value: key, label: `[Plugin] ${key}` }); } }); } types.forEach(t => { const opt = document.createElement('option'); opt.value = t.value; opt.innerText = t.label; opt.selected = this.data[`type${i}`] === t.value; typeSelect.appendChild(opt); }); const contentDiv = document.createElement('div'); typeSelect.addEventListener('change', (e) => { this.data[`type${i}`] = e.target.value; this._renderTypeSpecificInput(i, contentDiv); }); controlsWrapper.appendChild(typeSelect); headerRow.appendChild(controlsWrapper); colDiv.appendChild(headerRow); colDiv.appendChild(contentDiv); // --- Sub-Plugin (Sub-Content) Section --- const subWrapper = document.createElement('div'); if (this.data[`subType${i}`] !== 'none') { subWrapper.className = 'mt-3 pt-2 border-top'; } const subHeader = document.createElement('div'); subHeader.className = 'd-flex justify-content-between align-items-center mb-2'; const subLabel = document.createElement('label'); subLabel.className = 'small font-weight-bold text-muted mb-0'; subLabel.style.fontSize = '10px'; subLabel.innerText = 'SUB-CONTENT / INJECT PLUGIN'; subHeader.appendChild(subLabel); const subTypeSelect = document.createElement('select'); subTypeSelect.className = 'custom-select custom-select-sm'; subTypeSelect.style.width = '130px'; subTypeSelect.style.fontSize = '10px'; if (this.readOnly) subTypeSelect.disabled = true; const subTypes = [ { value: 'none', label: 'None' }, { value: 'caption', label: 'Text Caption' }, { value: 'html', label: 'HTML/Text' }, { value: 'image', label: 'Image' }, { value: 'youtube', label: 'YouTube' }, { value: 'accordion', label: 'Accordion' } ]; if (window.SISEditorPlugins) { Object.keys(window.SISEditorPlugins).forEach(key => { if (!subTypes.some(t => t.value === key)) { subTypes.push({ value: key, label: `[Plugin] ${key}` }); } }); } subTypes.forEach(t => { const opt = document.createElement('option'); opt.value = t.value; opt.innerText = t.label; opt.selected = this.data[`subType${i}`] === t.value; subTypeSelect.appendChild(opt); }); subHeader.appendChild(subTypeSelect); subWrapper.appendChild(subHeader); const subContentDiv = document.createElement('div'); subWrapper.appendChild(subContentDiv); colDiv.appendChild(subWrapper); const renderSubInputs = () => { subContentDiv.innerHTML = ''; const subType = this.data[`subType${i}`]; if (subType === 'none') { subWrapper.className = ''; } else { subWrapper.className = 'mt-3 pt-2 border-top'; } if (subType === 'none') { // Empty } else if (subType === 'caption') { const captionGroup = document.createElement('div'); captionGroup.className = 'form-group mb-2'; const captionInput = document.createElement('input'); captionInput.type = 'text'; captionInput.className = 'form-control form-control-sm'; captionInput.style.fontSize = '11px'; captionInput.placeholder = 'Optional column caption / text...'; captionInput.value = this.data[`caption${i}`] || ''; if (this.readOnly) captionInput.disabled = true; captionInput.addEventListener('input', (e) => { this.data[`caption${i}`] = e.target.value; }); captionGroup.appendChild(captionInput); subContentDiv.appendChild(captionGroup); } else { this._renderTypeSpecificInput(`sub_${i}`, subContentDiv, subType); } }; subTypeSelect.addEventListener('change', (e) => { this.data[`subType${i}`] = e.target.value; renderSubInputs(); }); row.appendChild(colDiv); this._renderTypeSpecificInput(i, contentDiv); renderSubInputs(); } this.inputsContainer.appendChild(row); } _renderTypeSpecificInput(i, container, explicitType) { container.innerHTML = ''; // Remove tracking of previous instance if (this.activeInstances[i]) { delete this.activeInstances[i]; } const typeContainer = document.createElement('div'); typeContainer.className = 'mb-3'; container.appendChild(typeContainer); const type = explicitType || this.data[`type${i}`] || 'html'; // Check if type is a dynamic editor plugin registered in window.SISEditorPlugins if (window.SISEditorPlugins && window.SISEditorPlugins[type]) { try { const pluginEntry = window.SISEditorPlugins[type]; const pluginClass = pluginEntry.class; const pluginConfig = pluginEntry.config || {}; let parsedData = {}; if (this.data[`col${i}`]) { try { parsedData = typeof this.data[`col${i}`] === 'string' ? JSON.parse(this.data[`col${i}`]) : this.data[`col${i}`]; } catch (e) { parsedData = { html: this.data[`col${i}`] }; // fallback } } const instance = new pluginClass({ data: parsedData, api: this.api, readOnly: this.readOnly, config: pluginConfig, block: { id: 'nested-flex-' + i + '-' + Math.random().toString(36).substring(7), name: type, holder: typeContainer, isEmpty: false, selected: false, stretched: false, tunes: {} } }); this.activeInstances[i] = instance; const element = instance.render(); typeContainer.appendChild(element); // Fix for Editor.js Image plugin 404 errors (spinner hangs forever) if (type === 'image') { // Custom UI override for nested images const injectCustomButtons = () => { if (element.querySelector('.sis-custom-image-controls')) return; const fileBtn = element.querySelector('.cdx-button'); if (fileBtn) { fileBtn.style.display = 'none'; // Hide native button } const controls = document.createElement('div'); controls.className = 'sis-custom-image-controls mt-2 p-2 border rounded bg-light'; controls.style.display = 'flex'; controls.style.flexDirection = 'column'; controls.style.gap = '8px'; controls.innerHTML = `